/*
 * GKOMatch.java
 * Created on 15 juin 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2008, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.dao.match;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;

import com.hockeo.vhbase.data.dao.IGEntity;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.helper.GMatchResult;

/**
 * Encapsulates all matches between two clubs that belong to one KO round.
 *
 * @version $Id$
 * @author jjanke
 */
@Entity
@Table( name = "s_ko_match" )
public class GKOMatch implements IGEntity
{
  @Id
  @Column( name = "id" )
  @GeneratedValue( strategy = GenerationType.AUTO )
  private int             d_nId;

  @Column( name = "name" )
  private String          d_strName;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "contest_element_id" )
  private GContestElement d_contestElement;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "club_home_id" )
  private GClub           d_clubHome;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "club_away_id" )
  private GClub           d_clubAway;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "club_winner_id" )
  private GClub           d_clubWinner;

  @Column( name = "played_matches" )
  private int             d_nNumPlayedMatches = 0;

  @Column( name = "wins_home" )
  private int             d_nNumWinsHome      = 0;

  @Column( name = "wins_away" )
  private int             d_nNumWinsAway      = 0;

  @Column( name = "wins_ot_home" )
  private int             d_nNumWinsOTHome    = 0;

  @Column( name = "wins_ot_away" )
  private int             d_nNumWinsOTAway    = 0;

  @Column( name = "losses_home" )
  private int             d_nNumLossesHome    = 0;

  @Column( name = "losses_away" )
  private int             d_nNumLossesAway    = 0;

  @Column( name = "losses_ot_home" )
  private int             d_nNumLossesOTHome  = 0;

  @Column( name = "losses_ot_away" )
  private int             d_nNumLossesOTAway  = 0;

  @Column( name = "points_home" )
  private int             d_nNumPointsHome    = 0;

  @Column( name = "points_away" )
  private int             d_nNumPointsAway    = 0;

  @Column( name = "ecr_points_home" )
  private BigDecimal      d_decECRPointsHome  = new BigDecimal( "0.00" );

  @Column( name = "ecr_points_away" )
  private BigDecimal      d_decECRPointsAway  = new BigDecimal( "0.00" );

  @OneToMany( mappedBy = "d_koMatch", cascade = { CascadeType.ALL } )
  @OrderBy( "d_nId" )
  private List<GMatch>    d_listMatches;

  @Column( name = "played" )
  private boolean         d_fPlayed;

  /** Default no-argument constructor. */
  protected GKOMatch()
  {}

  /**
   * Creates a new KO match. The constructor also creates the single atomic matches that
   * make up a KO match series. The number of matches that are created depends on the
   * number of matchdays provided to this constructor. The home/away right changes in each
   * match. The home club plays at home in the first, third, fifth (impair) game and the
   * away club in the second, fourth, sixth (pair) game.
   *
   * @param element the contest element to which this KO match is attached
   * @param clubHome the first opponent
   * @param clubAway the second opponent
   * @param listMatchdays the matchdays for which matches will be created
   */
  public GKOMatch( final GContestElement element, final GClub clubHome, final GClub clubAway,
                   final List<GMatchday> listMatchdays )
  {
    d_contestElement = element;
    d_clubHome = clubHome;
    d_clubAway = clubAway;
    d_listMatches = new ArrayList<GMatch>();

    // Create the matches alternating home/away right
    //
    for ( int i = 0; i < listMatchdays.size(); i++ )
      if ( i % 2 == 0 )
        d_listMatches.add( new GMatch( this, listMatchdays.get( i ), clubHome, clubAway ) );
      else
        d_listMatches.add( new GMatch( this, listMatchdays.get( i ), clubAway, clubHome ) );
  }

  /**
   * Creates a new KO match. The constructor also creates the single atomic matches that
   * make up a KO match series. The number of matches that are created depends on the
   * number of matchdays provided to this constructor. The home/away right changes in each
   * match. The home club plays at home in the first, third, fifth (impair) game and the
   * away club in the second, fourth, sixth (pair) game.
   *
   * @param strName a match name like e.g. "QF1" for quarter-final 1 (optional)
   * @param element the contest element to which this KO match is attached
   * @param clubHome the first opponent
   * @param clubAway the second opponent
   * @param listMatchdays the matchdays for which matches will be created
   */
  public GKOMatch( final String strName, final GContestElement element, final GClub clubHome, final GClub clubAway,
                   final List<GMatchday> listMatchdays )
  {
    this( element, clubHome, clubAway, listMatchdays );

    d_strName = strName;
  }

  public GContestElement getContestElement()
  {
    return d_contestElement;
  }

  public GClub getClubHome()
  {
    return d_clubHome;
  }

  public GClub getClubAway()
  {
    return d_clubAway;
  }

  public List<GMatch> getMatches()
  {
    return d_listMatches;
  }

  /**
   * Determines whether the given club has won this KO series.
   *
   * @param club the club for which it should be checked if it has won the series or not
   * @return <code>true</code> if this KO match was won by the given club
   */
  public boolean isWinner( GClub club )
  {
    GClub clubWinner = getWinner();

    if ( clubWinner == null )
      throw new IllegalStateException( "No winner is yet known for KOMatch " + getId() );

    return club.equals( clubWinner );
  }

  /**
   * Determines whether the given club has lost this KO series.
   *
   * @param club the club for which it should be checked if it has lost the series or not
   * @return <code>true</code> if this KO match was lost by the given club
   */
  public boolean isLoser( GClub club )
  {
    GClub clubLoser = getLoser();

    if ( clubLoser == null )
      throw new IllegalStateException( "No loser is yet known for KOMatch " + getId() );

    return club.equals( clubLoser );
  }

  /**
   * Returns the club that lost this KO series. If no winner/loser has been determined
   * yet, it is tried to determine a loser. If not enough matches have been played so far,
   * no loser can be determined.
   *
   * @return the club having lost this KO series or <code>null</code> if no loser is
   *         determined yet
   */
  public GClub getLoser()
  {
    GClub clubWinner = getWinner();

    if ( clubWinner == null )
      return null;

    if ( clubWinner.equals( d_clubHome ) )
      return d_clubAway;
    else
      return d_clubHome;
  }

  /**
   * Returns the winner of this KO series. If no winner has been determined yet, it is
   * tried to determine a winner. If not enough matches have been played so far, no winner
   * can be determined.
   *
   * @return the club having won this KO series or <code>null</code> if no winner is
   *         determined yet
   */
  public GClub getWinner()
  {
    if ( d_clubWinner == null )
    {
      int nNumRequiredWins = getMinNumberOfMatchesToBePlayed();

      // Reset variables
      //
      d_nNumPointsHome = 0;
      d_nNumWinsHome = 0;
      d_nNumWinsOTHome = 0;
      d_nNumLossesHome = 0;
      d_nNumLossesOTHome = 0;

      d_nNumPointsAway = 0;
      d_nNumWinsAway = 0;
      d_nNumWinsOTAway = 0;
      d_nNumLossesAway = 0;
      d_nNumLossesOTAway = 0;

      for ( GMatch match : d_listMatches )
      {
        if ( !match.isPlayed() )
          break;

        GMatchResult result = match.getResult( d_clubHome );

        d_nNumPointsHome += result.getNumPoints();
        d_nNumPointsAway += result.getOppositeResult().getNumPoints();

        switch ( result )
        {
          case VICTORY:
            d_nNumWinsHome++;
            d_nNumLossesAway++;
            break;

          case VICTORY_AFTER_SHOOTOUT:
            d_nNumWinsOTHome++;
            d_nNumLossesOTAway++;
            break;

          case LOSS:
            d_nNumLossesHome++;
            d_nNumWinsAway++;
            break;

          case LOSS_AFTER_SHOOTOUT:
            d_nNumLossesOTHome++;
            d_nNumWinsOTAway++;
            break;

          default:
            throw new IllegalStateException( "Unexpected match result: " + result.name() );
        }
      }

      if ( d_nNumWinsHome + d_nNumWinsOTHome == nNumRequiredWins )
        d_clubWinner = d_clubHome;
      else if ( d_nNumWinsAway + d_nNumWinsOTAway == nNumRequiredWins )
        d_clubWinner = d_clubAway;

      // If we have a winner, we can calculate other variables as well
      //
      if ( d_clubWinner != null )
      {
        d_nNumPlayedMatches = d_nNumWinsHome + d_nNumWinsOTHome + d_nNumLossesHome + d_nNumLossesOTHome;

        d_decECRPointsHome = new BigDecimal( ( d_nNumPointsHome / (double) d_nNumPlayedMatches ) * nNumRequiredWins ).setScale(
            3, RoundingMode.HALF_UP );
        d_decECRPointsAway = new BigDecimal( ( d_nNumPointsAway / (double) d_nNumPlayedMatches ) * nNumRequiredWins ).setScale(
            3, RoundingMode.HALF_UP );
      }
    }

    return d_clubWinner;
  }

  public boolean isClubHomeWinner()
  {
    return isPlayed() && getWinner().equals( d_clubHome );
  }

  public boolean isClubAwayWinner()
  {
    return isPlayed() && getWinner().equals( d_clubAway );
  }

  /**
   * Returns the number of actually played matches.
   */
  public int getNumPlayedMatches()
  {
    return d_nNumPlayedMatches;
  }

  public int getNumWinsTotalHome()
  {
    return d_nNumWinsHome + d_nNumWinsOTHome;
  }

  public int getNumWinsTotalAway()
  {
    return d_nNumWinsAway + d_nNumWinsOTAway;
  }

  /**
   * Returns the number of points reached by the home team calculated based on the minimum
   * number of matches. I.e. if this is a best-of-three series, the amount of points is
   * calculated for two matches. A team winning 2 matches straight away gets 6 points, a
   * team winning the series in three matches only 4 etc.
   */
  public BigDecimal getECRPointsHome()
  {
    return d_decECRPointsHome;
  }

  /**
   * Returns the number of points reached by the visiting team calculated based on the
   * minimum number of matches. I.e. if this is a best-of-three series, the amount of
   * points is calculated for two matches. A team winning 2 matches straight away gets 6
   * points, a team winning the series in three matches only 4 etc.
   */
  public BigDecimal getECRPointsAway()
  {
    return d_decECRPointsAway;
  }

  public boolean isPlayed()
  {
    return d_fPlayed;
  }

  /**
   * Plays all matches that belong to this KOMatch.
   *
   * @param gaHome the goals account for the home team
   * @param gaAway the goals account for the away team
   */
  public void play( final GGoalsAccount gaHome, final GGoalsAccount gaAway )
  {
    int nNumMinMatches = getMinNumberOfMatchesToBePlayed();

    for ( int i = 0; i < d_listMatches.size(); i++ )
    {
      GMatch match = d_listMatches.get( i );

      // Some matches may not be required to be executed if a series winner is already
      // known. The call to getWinner() calculates who is the winner (if there is one),
      // the number of victories for each team, the number of points etc.
      //
      if ( i >= nNumMinMatches && getWinner() != null )
      {
        match.setNotRequired( true );
        continue;
      }

      // Alternate home/away with every match
      //
      if ( i % 2 == 0 )
        match.play( gaHome, gaAway );
      else
        match.play( gaAway, gaHome );
    }

    // Mark the series as being played
    //
    d_fPlayed = true;
  }

  /**
   * Calculates the mininum number of matches to be played in this KO match series.
   */
  private int getMinNumberOfMatchesToBePlayed()
  {
    int nNumMatches = d_listMatches.size();

    // If there are less than 3 matches or the number of matches to be played is even, all
    // matches must be played.
    //
    if ( nNumMatches < 3 || nNumMatches % 2 == 0 )
      return nNumMatches;

    return ( nNumMatches + 1 ) / 2;
  }

  /**
   * Returns the number of points of the given club.
   */
  public int getNumPoints( GClub club )
  {
    return club.equals( d_clubHome ) ? d_nNumPointsHome : d_nNumPointsAway;
  }

  /**
   * Returns the number of ECR points of the given club.
   */
  public BigDecimal getNumECRPoints( GClub club )
  {
    return club.equals( d_clubHome ) ? d_decECRPointsHome : d_decECRPointsAway;
  }

  /**
   * Returns the number of regular time wins of the given club.
   */
  public int getNumWins( GClub club )
  {
    return club.equals( d_clubHome ) ? d_nNumWinsHome : d_nNumWinsAway;
  }

  /**
   * Returns the number of shootout wins of the given club.
   */
  public int getNumWinsOT( GClub club )
  {
    return club.equals( d_clubHome ) ? d_nNumWinsOTHome : d_nNumWinsOTAway;
  }

  /**
   * Returns the number of regular time losses of the given club.
   */
  public int getNumLosses( GClub club )
  {
    return club.equals( d_clubHome ) ? d_nNumLossesHome : d_nNumLossesAway;
  }

  /**
   * Returns the number of shootout losses of the given club.
   */
  public int getNumLossesOT( GClub club )
  {
    return club.equals( d_clubHome ) ? d_nNumLossesOTHome : d_nNumLossesOTAway;
  }

  public String getName()
  {
    return d_strName;
  }

  public int getId()
  {
    return d_nId;
  }

  public Class<? extends IGEntity> getGEntityClass()
  {
    return GKOMatch.class;
  }

  public boolean instanceOf( Class<? extends IGEntity> classOther )
  {
    return classOther.isAssignableFrom( getGEntityClass() );
  }

  /*
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals( Object obj )
  {
    try
    {
      return d_nId == ( (GKOMatch) obj ).getId();
    }
    catch ( Exception ex ) // especially ClassCastException(s) may happen
    {
      return false;
    }
  }

  /*
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode()
  {
    return d_nId;
  }

  /**
   * Returns the ko match id followed by the names of the two clubs affronting each other.
   * The home team of the first match is mentionned first.
   *
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString()
  {
    return String.valueOf( d_nId ) + " ( " + d_clubHome.getName() + " - " + d_clubAway.getName() + " )";
  }
}
